import Sortable from "sortablejs";
import { insertNodeAt, removeNode } from "./util/htmlHelper";
import { console } from "./util/console";
import { getComponentAttributes, createSortableOption, getValidSortableEntries } from "./core/componentBuilderHelper";
import { computeComponentStructure } from "./core/renderHelper";
import { events } from "./core/sortableEvents";
import { h, defineComponent, nextTick } from "vue";

function emit(evtName, evtData) {
	nextTick(() => this.$emit(evtName.toLowerCase(), evtData));
}

function manage(evtName) {
	return (evtData, originalElement) => {
		if (this.realList !== null) {
			return this[`onDrag${evtName}`](evtData, originalElement);
		}
	};
}

function manageAndEmit(evtName) {
	const delegateCallBack = manage.call(this, evtName);
	return (evtData, originalElement) => {
		delegateCallBack.call(this, evtData, originalElement);
		emit.call(this, evtName, evtData);
	};
}

let draggingElement = null;

const props = {
	list: {
		type: Array,
		required: false,
		default: null,
	},
	modelValue: {
		type: Array,
		required: false,
		default: null,
	},
	itemKey: {
		type: [String, Function],
		required: true,
	},
	clone: {
		type: Function,
		default: (original) => {
			return original;
		},
	},
	tag: {
		type: String,
		default: "div",
	},
	move: {
		type: Function,
		default: null,
	},
	componentData: {
		type: Object,
		required: false,
		default: null,
	},
};

const emits = ["update:modelValue", "change", ...[...events.manageAndEmit, ...events.emit].map((evt) => evt.toLowerCase())];

const draggableComponent = defineComponent({
	name: "draggable",

	inheritAttrs: false,

	props,

	emits,

	data() {
		return {
			error: false,
		};
	},

	render() {
		try {
			this.error = false;
			const { $slots, $attrs, tag, componentData, realList, getKey } = this;
			const componentStructure = computeComponentStructure({
				$slots,
				tag,
				realList,
				getKey,
			});
			this.componentStructure = componentStructure;
			const attributes = getComponentAttributes({ $attrs, componentData });
			return componentStructure.render(h, attributes);
		} catch (err) {
			this.error = true;
			return h("pre", { style: { color: "red" } }, err.stack);
		}
	},

	created() {
		if (this.list !== null && this.modelValue !== null) {
			console.error("modelValue and list props are mutually exclusive! Please set one or another.");
		}
	},

	mounted() {
		if (this.error) {
			return;
		}

		const { $attrs, $el, componentStructure } = this;
		componentStructure.updated();

		const sortableOptions = createSortableOption({
			$attrs,
			callBackBuilder: {
				manageAndEmit: (event) => manageAndEmit.call(this, event),
				emit: (event) => emit.bind(this, event),
				manage: (event) => manage.call(this, event),
			},
		});
		const targetDomElement = $el.nodeType === 1 ? $el : $el.parentElement;
		this._sortable = new Sortable(targetDomElement, sortableOptions);
		this.targetDomElement = targetDomElement;
		targetDomElement.__draggable_component__ = this;
	},

	updated() {
		if (this.componentStructure && this.componentStructure.updated) {
			this.componentStructure.updated();
		}
	},

	beforeUnmount() {
		if (this._sortable !== undefined) this._sortable.destroy();
	},

	computed: {
		realList() {
			const { list } = this;
			return list ? list : this.modelValue;
		},

		getKey() {
			const { itemKey } = this;
			if (typeof itemKey === "function") {
				return itemKey;
			}
			return (element) => element[itemKey];
		},
	},

	watch: {
		$attrs: {
			handler(newOptionValue) {
				const { _sortable } = this;
				if (!_sortable) return;
				getValidSortableEntries(newOptionValue).forEach(([key, value]) => {
					_sortable.option(key, value);
				});
			},
			deep: true,
		},
	},

	methods: {
		getUnderlyingVm(domElement) {
			return this.componentStructure.getUnderlyingVm(domElement) || null;
		},

		getUnderlyingPotencialDraggableComponent(htmElement) {
			//TODO check case where you need to see component children
			return htmElement.__draggable_component__;
		},

		emitChanges(evt) {
			nextTick(() => this.$emit("change", evt));
		},

		alterList(onList) {
			if (this.list) {
				onList(this.list);
				return;
			}
			const newList = [...this.modelValue];
			onList(newList);
			this.$emit("update:modelValue", newList);
		},

		spliceList() {
			const spliceList = (list) => list.splice(...arguments);
			this.alterList(spliceList);
		},

		updatePosition(oldIndex, newIndex) {
			const updatePosition = (list) => list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
			this.alterList(updatePosition);
		},

		getRelatedContextFromMoveEvent({ to, related }) {
			const component = this.getUnderlyingPotencialDraggableComponent(to);
			if (!component) {
				return { component };
			}
			const list = component.realList;
			const context = { list, component };
			if (to !== related && list) {
				const destination = component.getUnderlyingVm(related) || {};
				return { ...destination, ...context };
			}
			return context;
		},

		getVmIndexFromDomIndex(domIndex) {
			return this.componentStructure.getVmIndexFromDomIndex(domIndex, this.targetDomElement);
		},

		onDragStart(evt) {
			this.context = this.getUnderlyingVm(evt.item);
			evt.item._underlying_vm_ = this.clone(this.context.element);
			draggingElement = evt.item;
		},

		onDragAdd(evt) {
			const element = evt.item._underlying_vm_;
			if (element === undefined) {
				return;
			}
			removeNode(evt.item);
			const newIndex = this.getVmIndexFromDomIndex(evt.newIndex);
			this.spliceList(newIndex, 0, element);
			const added = { element, newIndex };
			this.emitChanges({ added });
		},

		onDragRemove(evt) {
			insertNodeAt(this.$el, evt.item, evt.oldIndex);
			if (evt.pullMode === "clone") {
				removeNode(evt.clone);
				return;
			}
			const { index: oldIndex, element } = this.context;
			this.spliceList(oldIndex, 1);
			const removed = { element, oldIndex };
			this.emitChanges({ removed });
		},

		onDragUpdate(evt) {
			removeNode(evt.item);
			insertNodeAt(evt.from, evt.item, evt.oldIndex);
			const oldIndex = this.context.index;
			const newIndex = this.getVmIndexFromDomIndex(evt.newIndex);
			this.updatePosition(oldIndex, newIndex);
			const moved = { element: this.context.element, oldIndex, newIndex };
			this.emitChanges({ moved });
		},

		computeFutureIndex(relatedContext, evt) {
			if (!relatedContext.element) {
				return 0;
			}
			const domChildren = [...evt.to.children].filter((el) => el.style["display"] !== "none");
			const currentDomIndex = domChildren.indexOf(evt.related);
			const currentIndex = relatedContext.component.getVmIndexFromDomIndex(currentDomIndex);
			const draggedInList = domChildren.indexOf(draggingElement) !== -1;
			return draggedInList || !evt.willInsertAfter ? currentIndex : currentIndex + 1;
		},

		onDragMove(evt, originalEvent) {
			const { move, realList } = this;
			if (!move || !realList) {
				return true;
			}

			const relatedContext = this.getRelatedContextFromMoveEvent(evt);
			const futureIndex = this.computeFutureIndex(relatedContext, evt);
			const draggedContext = {
				...this.context,
				futureIndex,
			};
			const sendEvent = {
				...evt,
				relatedContext,
				draggedContext,
			};
			return move(sendEvent, originalEvent);
		},

		onDragEnd() {
			draggingElement = null;
		},
	},
});

export default draggableComponent;
